/*
 * Copyright Konstantin Triger <kostat@gmail.com> 
 * 
 * This file is part of Jaque 2 JPA - JAva QUEry 2 JPA library <http://code.google.com/p/jaque/>.
 * 
 * Jaque is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Jaque is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package jaque.jpa;

import java.lang.reflect.*;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.*;

import jaque.expressions.*;

final class JPQLFunctionNormalizer extends SimpleExpressionVisitor {

	private final static HashMap<String, Method> _nameMethodMapper;

	static final Method ObjectEquals;
	// static final Method StringLength;
	// static final Method StringIndexOf;
	// static final Method StringIndexOf2;
	// static final Method StringToLowerCase;
	// static final Method StringToUpperCase;
	// static final Method StringTrim;
	// static final Method StringSubstring;
	// static final Method StringSubstring2;
	//	
	// static final Method StringConcat;
	static final Method CollectionSize;

	static final Method JPQLFunctionNormalizerLength;
	static final Method JPQLFunctionNormalizerIndexOf;
	static final Method JPQLFunctionNormalizerIndexOf2;
	static final Method JPQLFunctionNormalizerToLowerCase;
	static final Method JPQLFunctionNormalizerToUpperCase;
	static final Method JPQLFunctionNormalizerTrim;
	static final Method JPQLFunctionNormalizerSubstring;
	static final Method JPQLFunctionNormalizerSubstring2;
	static final Method JPQLFunctionNormalizerConcat;
	static final Method JPQLFunctionNormalizerCurrentTime;
	static final Method JPQLFunctionNormalizerCurrentDate;
	static final Method JPQLFunctionNormalizerCurrentTimestamp;
	static final Method JPQLFunctionNormalizerLE;
	static final Method JPQLFunctionNormalizerGE;
	static final Method JPQLFunctionNormalizerSize;

	static {
		try {
			ObjectEquals = Object.class.getMethod("equals", Object.class);
			CollectionSize = Collection.class.getMethod("size");

			JPQLFunctionNormalizerLength = JPQLFunctionNormalizer.class
					.getMethod("length", String.class);
			JPQLFunctionNormalizerIndexOf = JPQLFunctionNormalizer.class
					.getMethod("indexOf", String.class, String.class);
			JPQLFunctionNormalizerIndexOf2 = JPQLFunctionNormalizer.class
					.getMethod("indexOf", String.class, String.class,
							Integer.TYPE);
			JPQLFunctionNormalizerToLowerCase = JPQLFunctionNormalizer.class
					.getMethod("toLowerCase", String.class);
			JPQLFunctionNormalizerToUpperCase = JPQLFunctionNormalizer.class
					.getMethod("toUpperCase", String.class);
			JPQLFunctionNormalizerTrim = JPQLFunctionNormalizer.class
					.getMethod("trim", String.class);
			JPQLFunctionNormalizerSubstring = JPQLFunctionNormalizer.class
					.getMethod("substring", String.class, Integer.TYPE);
			JPQLFunctionNormalizerSubstring2 = JPQLFunctionNormalizer.class
					.getMethod("substring", String.class, Integer.TYPE,
							Integer.TYPE);
			JPQLFunctionNormalizerConcat = JPQLFunctionNormalizer.class
					.getMethod("concat", String.class, String.class);
			JPQLFunctionNormalizerCurrentDate = JPQLFunctionNormalizer.class
					.getMethod("currentDate");
			JPQLFunctionNormalizerCurrentTime = JPQLFunctionNormalizer.class
					.getMethod("currentTime");
			JPQLFunctionNormalizerCurrentTimestamp = JPQLFunctionNormalizer.class
					.getMethod("currentTimestamp");
			JPQLFunctionNormalizerLE = JPQLFunctionNormalizer.class.getMethod(
					"lessThanOrEqual", Date.class, Date.class);
			JPQLFunctionNormalizerGE = JPQLFunctionNormalizer.class.getMethod(
					"greaterThanOrEqual", Date.class, Date.class);
			JPQLFunctionNormalizerSize = JPQLFunctionNormalizer.class
					.getMethod("size", Collection.class);
		} catch (NoSuchMethodException e) {
			throw new RuntimeException(e);
		}

		_nameMethodMapper = new HashMap<String, Method>();
		// _nameMethodMapper.put("equals", ObjectEquals);
		// _nameMethodMapper.put("size", CollectionSize);
	}

	private JPQLFunctionNormalizer() {
	}

	static boolean equals(Method me, Method other) {
		if (me.getDeclaringClass().isAssignableFrom(other.getDeclaringClass())) {
			if (!me.getReturnType().isAssignableFrom(other.getReturnType()))
				return false;
		} else {
			if (other.getDeclaringClass().isAssignableFrom(
					me.getDeclaringClass())) {
				if (!other.getReturnType().isAssignableFrom(me.getReturnType()))
					return false;
			} else
				return false;
		}

		Class<?>[] params1 = me.getParameterTypes();
		Class<?>[] params2 = other.getParameterTypes();
		if (params1.length == params2.length) {
			for (int i = 0; i < params1.length; i++) {
				if (params1[i] != params2[i])
					return false;
			}
			return true;
		}

		return false;
	}

	public static final JPQLFunctionNormalizer Instance = new JPQLFunctionNormalizer();

	@Override
	public Expression visit(MemberExpression e) {
		// if (e.getExpressionType() == ExpressionType.MethodAccess)
		// return super.visit(e);

		Member me = e.getMember();
		Method base = _nameMethodMapper.get(me.getName());

		if (base == null || !equals((Method) me, base))
			return super.visit(e);

		if (base == ObjectEquals)
			return Expression.member(ExpressionType.MethodAccess, e
					.getInstance().apply(this), base, base.getReturnType(), e
					.getParameters());

		Method mapped = JPQLFunctionNormalizerSize;

		Class<?>[] params = mapped.getParameterTypes();
		ParameterExpression[] paramExps = new ParameterExpression[params.length];

		for (int i = 0; i < params.length; i++)
			paramExps[i] = Expression.parameter(params[i], i);

		return Expression.member(ExpressionType.MethodAccess, null, mapped,
				mapped.getReturnType(), Arrays.asList(paramExps));
	}

	@Override
	public Expression visit(InvocationExpression e) {
		if (e.getMethod().getExpressionType() == ExpressionType.FieldAccess)
			return super.visit(e);

		MemberExpression orig = (MemberExpression) e.getMethod();
		MemberExpression expr = (MemberExpression) orig.apply(this);

		List<Expression> args = visitExpressionList(e.getArguments());
		if (expr != orig) {
			List<Expression> finalArgs = args;
			if (orig.getInstance() != null && expr.getInstance() == null) {
				finalArgs = new ArrayList<Expression>(1 + args.size());
				finalArgs.add(orig.getInstance().apply(this));
				finalArgs.addAll(args);
			}

			return Expression.invoke((InvocableExpression) expr, finalArgs);
		}

		if (args != e.getArguments() || expr != orig)
			return Expression.invoke((InvocableExpression) expr, args);

		return e;
	}

	// String functions
	public static int length(String source) { // LENGTH
		throw new UnsupportedOperationException();
	}

	public static int indexOf(String source, String target) { // LOCATE
		throw new UnsupportedOperationException();
	}

	public static int indexOf(String source, String target, int beginIndex) { // LOCATE
		throw new UnsupportedOperationException();
	}

	public static int toLowerCase(String source) { // LOWER
		throw new UnsupportedOperationException();
	}

	public static int toUpperCase(String source) { // UPPER
		throw new UnsupportedOperationException();
	}

	public static int trim(String source) { // TRIM
		throw new UnsupportedOperationException();
	}

	public static int substring(String source, int beginIndex) { // SUBSTRING
		throw new UnsupportedOperationException();
	}

	public static int substring(String source, int beginIndex, int length) { // SUBSTRING
		throw new UnsupportedOperationException();
	}

	public static String concat(String source, String target) { // CONCAT
		throw new UnsupportedOperationException();
	}

	// DateTime
	public static java.sql.Date currentDate() { // CURRENT_DATE
		throw new UnsupportedOperationException();
	}

	public static Time currentTime() { // CURRENT_TIME
		throw new UnsupportedOperationException();
	}

	public static Timestamp currentTimestamp() { // CURRENT_TIMESTAMP
		throw new UnsupportedOperationException();
	}

	public static boolean lessThanOrEqual(Date source, Date target) { // <=
		throw new UnsupportedOperationException();
	}

	public static boolean greaterThanOrEqual(Date source, Date target) { // >=
		throw new UnsupportedOperationException();
	}

	// arithmetic
	public static int size(Collection<?> source) { // SIZE
		throw new UnsupportedOperationException();
	}
}
